package boardgamer.yavalath.server;

import boardgamer.common.CyclicIterator;
import boardgamer.common.GameUpdate;
import boardgamer.common.net.PlayerId;
import boardgamer.server.GameServer;
import boardgamer.yavalath.common.YavalathBoard;
import boardgamer.yavalath.common.YavalathException;
import boardgamer.yavalath.common.YavalathGameServer;
import boardgamer.yavalath.common.YavalathGameSetting;
import boardgamer.yavalath.common.YavalathGameState;
import boardgamer.yavalath.common.YavalathGameState.Phase;
import boardgamer.yavalath.common.YavalathGameStateChange;
import boardgamer.yavalath.common.YavalathGameStateChange.PlayerJoinGame;
import boardgamer.yavalath.common.YavalathGameStateChange.StartGame;
import boardgamer.yavalath.common.YavalathPlayerState;
import boardgamer.yavalath.common.YavalathPlayerState.PlayerColor;
import boardgamer.yavalath.common.YavalathReplay;
import boardgamer.yavalath.server.YavalathGameStateUpdater.Pair;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

public class YavalathGameServerImpl
    extends GameServer<YavalathGameState, YavalathGameStateChange>
    implements YavalathGameServer {

  public YavalathGameServerImpl(YavalathGameSetting gameSetting) {
    super(new YavalathGameState.Builder()
        .phase(Phase.PLAYER_SETUP)
        .playerStates(ImmutableMap.<PlayerId, YavalathPlayerState>of())
        .activePlayers(new CyclicIterator<PlayerId>())
        .board(new YavalathBoard(
            gameSetting.getBoardSize().getSize(),
            gameSetting.getRule().getLoseLength(),
            gameSetting.getRule().getWinLength()))
        .build());
  }

  @Override
  public void addToken(PlayerId player, int x, int y) throws YavalathException {
    synchronized (syncState) {
      ImmutableList<Pair<YavalathGameState, YavalathGameStateChange>> updates
          = YavalathGameStateUpdater.getUpdatedGameStateAddToken(syncState.get(), player, x, y);
      for (Pair<YavalathGameState, YavalathGameStateChange> update : updates) {
        onNewGameState(update.first, update.second);
      }
    }
  }

  @Override
  public YavalathReplay getReplay() {
    return new YavalathReplay(getGameLog());
  }

  @Override
  public ImmutableList<GameUpdate<YavalathGameState, YavalathGameStateChange>>
      refresh(int firstTimestamp) {
    return getUpdates(firstTimestamp);
  }

  @Override
  public void joinGame(PlayerId player, YavalathPlayerState playerSetting) {
    synchronized (syncState) {
      final YavalathGameState state = syncState.get();
      // Make sure that operation is allowed
      checkState(state.getPhase() == Phase.PLAYER_SETUP, "Cannot join game in state: "
          + state.getPhase());
      checkState(state.getPlayerStates().size() < 3 || state.getPlayerStates().containsKey(player),
          "Game is full");

      Map<PlayerId, YavalathPlayerState> playerStates = state.getPlayerStates();

      // Check if the player is setting the same setting
      if (!playerStates.containsKey(player) || !playerStates.get(player).equals(playerSetting)) {
        Set<PlayerColor> colors = ImmutableSet.copyOf(Iterables.transform(playerStates.values(),
            new Function<YavalathPlayerState, PlayerColor>() {
              @Override
              public PlayerColor apply(YavalathPlayerState setting) {
                return setting.getColor();
              }
            }));
        checkArgument(!colors.contains(playerSetting.getColor()), "Another player has color: "
            + playerSetting.getColor());

        LinkedHashMap<PlayerId, YavalathPlayerState> hashPlayerStates
            = Maps.newLinkedHashMap(playerStates);
        hashPlayerStates.put(player, playerSetting);
        ImmutableMap<PlayerId, YavalathPlayerState> newPlayerStates
            = ImmutableMap.copyOf(hashPlayerStates);

        // Update the current game state
        onNewGameState(
            YavalathGameState.Builder.fromPrototype(state)
                .playerStates(newPlayerStates)
                .build(),
            new PlayerJoinGame(player, playerSetting));
      }
    }
  }

  @VisibleForTesting
  void setGameState(YavalathGameState gameState) {
    synchronized (syncState) {
      syncState.set(gameState);
    }
  }

  @VisibleForTesting
  YavalathGameState getGameState() {
    return syncState.get();
  }

  @Override
  public void startGame() {
    synchronized (syncState) {
      YavalathGameState state = syncState.get();
      Set<PlayerId> players = state.getPlayerStates().keySet();
      int playerCount = players.size();
      checkState(playerCount >= 2 && playerCount <= 3, "Invalid player count: " + playerCount);

      onNewGameState(
          YavalathGameState.Builder.fromPrototype(state)
              .phase(Phase.GAME)
              .activePlayers(new CyclicIterator<PlayerId>(ImmutableList.copyOf(players)))
              .build(),
          new StartGame());
    }
  }
}
